﻿using System;
using System.Drawing;
using SoftwareConsulting.BI11.Studio.InternalServices;

namespace SoftwareConsulting.BI11.NumericalMethods.Geometry
{
    /// <summary>
    /// Метод нахождения расстояний в пространстве
    /// </summary>
    public enum DistanceMethod
    {
        None = 0,
        Euclid = 1, //эвклид
        EuclidSquared = 2, //квадрат эвклида        
        EstimatedEuclid = 3, //взвешанный эвклид
        Hamming = 4, //хэмминг
        UrbanNeighborhoods = 5, //городские кварталы (расстояние Чебышева)
        DisagreementPercent = 6 //процент несогласия
    }

    /// <summary>
    /// Метод заполнения пропусков
    /// </summary>
    public enum MissingDataMethod
    {
        None = 0,
        Average = 1 //заполнять средними, ближайших имеющихся точек
    }

    /// <summary>
    /// Класс геометрических методов
    /// </summary>
    public static class GeometricMethods
    {
        /// <summary>
        /// По точке возвращает симметричную ей точку относительно начала системы координат
        /// </summary>
        /// <param name="originalPoint"></param>
        /// <returns></returns>
        public static Point GetNegativePoint(Point originalPoint)
        {
            return new Point(-originalPoint.X, -originalPoint.Y);
        }

        /// <summary>
        /// Возвращает расстояние между двумя точками в двумерном пространстве, используя формулу Эвклида
        /// </summary>
        /// <param name="point1"></param>
        /// <param name="point2"></param>
        /// <returns></returns>
        public static double CalculateDistance(Point point1, Point point2)
        {
            return CalculateDistance(new double[] { point1.X, point1.Y }, new double[] { point2.X, point2.Y }, DistanceMethod.Euclid);
        }

        /// <summary>
        /// Возвращает расстояние между двумя точками, используя формулу Эвклида с весами
        /// </summary>
        /// <param name="coordinates1">координаты 1й точки</param>
        /// <param name="coordinates2">координаты 2й точки</param>
        /// <param name="coordinatesWeights">веса координат (любые целые неотрицательные числа, хотя бы одно из которых не равно нулю)</param>
        /// <returns></returns>
        public static double CalculateDistance(double[] coordinates1, double[] coordinates2, int[] coordinatesWeights)
        {
            return CalculateDistance(coordinates1, coordinates2, DistanceMethod.EstimatedEuclid, coordinatesWeights);
        }

        /// <summary>
        /// Возвращает расстояние между двумя точками
        /// </summary>
        /// <param name="coordinates1">координаты 1й точки</param>
        /// <param name="coordinates2">координаты 2й точки</param>
        /// <param name="method">метод нахождения расстояния</param>
        /// <returns></returns>
        public static double CalculateDistance(double[] coordinates1, double[] coordinates2, DistanceMethod method)
        {
            return CalculateDistance(coordinates1, coordinates2, method, new int[] { });
        }

        /// <summary>
        /// Возвращает расстояние между двумя точками
        /// </summary>
        /// <param name="coordinates1">координаты 1й точки</param>
        /// <param name="coordinates2">координаты 2й точки</param>
        /// <param name="method">метод нахождения расстояния</param>
        /// <param name="coordinatesWeights">веса координат (любые целые неотрицательные числа, хотя бы одно из которых не равно нулю)</param>
        /// <returns></returns>
        private static double CalculateDistance(double[] coordinates1, double[] coordinates2, DistanceMethod method, int[] coordinatesWeights)
        {
            if (coordinates1.Length != coordinates2.Length)
                throw new BIGeometryCoordinatesCountMismatch(coordinates1.Length, coordinates2.Length, null);
            double[] normedCoordinatesWeights = new double[coordinatesWeights.Length];
            if (coordinatesWeights.Length == 0)
                method = method == DistanceMethod.EstimatedEuclid ? DistanceMethod.None : method;
            else
            {
                if (coordinatesWeights.Length != coordinates1.Length)
                    throw new BIGeometryCoordinatesCountMismatch(coordinatesWeights.Length, coordinates1.Length, null);
                int sum = 0;
                for (int i = 0; i < coordinatesWeights.Length; i++)
                {
                    if (coordinatesWeights[i] < 0)
                        throw new BIGeometryIncorrectCoordinateWeightException(i, coordinatesWeights[i], null);
                    sum += coordinatesWeights[i];
                }
                if (sum == 0)
                    throw new BIGeometryCoordinatesWeightsNotDefinedException(null);
                for (int i = 0; i < coordinatesWeights.Length; i++)
                {
                    normedCoordinatesWeights[i] = (double)coordinatesWeights[i] / (double)sum;
                }
            }
            bool[] haveValuableCoordinates = new bool[2];
            haveValuableCoordinates[0] = false;
            haveValuableCoordinates[1] = false;
            double result = 0;            
            Func<double, int, double> resultCycleProcessor = null;//первый вход - разность значений признака, второй вход - индекс признака, выход - результат текущего прохода цикла
            Func<double, double> resultProcessor = null;//вход - результат цикла, выход - итоговый результат
            switch (method)
            {
                case DistanceMethod.Euclid:                    
                case DistanceMethod.EuclidSquared:
                    resultCycleProcessor = (difference, i) => Math.Pow(difference, 2);
                    resultProcessor = result_ => ((method == DistanceMethod.Euclid) ? Math.Sqrt(result_) : result_);                    
                    break;
                case DistanceMethod.EstimatedEuclid:
                    resultCycleProcessor = (difference, i) => Math.Pow(difference, 2) * normedCoordinatesWeights[i];
                    resultProcessor = result_ => Math.Sqrt(result_);                    
                    break;
                case DistanceMethod.Hamming:
                    resultCycleProcessor = (difference, i) => Math.Abs(difference);
                    resultProcessor = result_ => result_;                    
                    break;
                case DistanceMethod.UrbanNeighborhoods:                    
                    resultCycleProcessor = (difference, i) => (Math.Abs(difference) > result ? -result + Math.Abs(difference) : 0);
                    resultProcessor = result_ => result_;                                        
                    break;
                case DistanceMethod.DisagreementPercent:
                    resultCycleProcessor = (difference, i) => (difference != 0 ? 1 : 0);
                    resultProcessor = result_ => ((double)result_ / (double)coordinates1.Length);                    
                    break;
                default:
                    throw new BIGeometryDistanceMethodNotDefinedException(null);
            }            
            for (int i = 0; i < coordinates1.Length; i++)
            {
                bool firstValuable = coordinates1[i] != Constants.NullDouble;
                bool secondValuable = coordinates2[i] != Constants.NullDouble;
                if (firstValuable)
                    haveValuableCoordinates[0] = true;
                if (secondValuable)
                    haveValuableCoordinates[1] = true;
                double difference = firstValuable && secondValuable ? coordinates1[i] - coordinates2[i] : 0;
                result += resultCycleProcessor(difference, i);
            }
            result = resultProcessor(result);
            if (!haveValuableCoordinates[0] || !haveValuableCoordinates[1])
                throw new BIGeometryImpossibleCalculateDistanceException(null);
            return result;
        }

        /// <summary>
        /// Заполняет пропуски в указанном массиве значений
        /// </summary>
        /// <param name="values">массив значений</param>
        /// <param name="missingDataMethod">метод заполнения пропусков</param>
        /// <param name="fillingDeep">глубина заполнения для указанного метода заполнения пропусков</param>
        /// <remarks>обозначением пропуска во входном массиве служит Constants.NullDouble</remarks>
        /// <returns></returns>
        public static void FillGaps(ref double[] values, MissingDataMethod missingDataMethod, int fillingDeep)
        {
            for (int i = 0; i < values.Length; i++)
            {
                if (values[i] == Constants.NullDouble)
                {
                    switch(missingDataMethod)
                    {
                        case MissingDataMethod.None:
                            //ничего не делаем, пропускаем
                            break;
                        case MissingDataMethod.Average:
                            //заполняем средними арифметическими ближайших значений
                            double leftVals = 0;
                            int leftValsCount = 0;
                            double rightVals = 0;
                            int rightValsCount = 0;
                            for (int j = i - 1; (j >= 0) && (j >= i - fillingDeep); j--)
                            {
                                if (values[j] != Constants.NullDouble)
                                {
                                    leftVals += values[j];
                                    leftValsCount++;
                                }
                            }
                            for (int j = i + 1; (j < values.Length) && (j < i + fillingDeep); j++)
                            {
                                if (values[j] != Constants.NullDouble)
                                {
                                    rightVals += values[j];
                                    rightValsCount++;
                                }
                            }
                            if ((leftValsCount == 0) && (rightValsCount == 0))
                            {
                                //нет значимых соседей, не можем заполнить этим методом при данной глубине                               
                            }
                            else
                                values[i] = (leftVals + rightVals) / (leftValsCount + rightValsCount);
                            break;                            
                        default:
                            Assert.Fail(Assert.Id.Assert00312);
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Возвращает центр множества точек
        /// </summary>
        /// <param name="missingDataMethod">метод заполнения пропусков</param>
        /// <param name="fillingDeep">глубина заполнения для указанного метода заполнения пропусков</param>        
        /// <param name="points">массив координат точек</param>
        /// <returns></returns>
        public static double[] CalculateAreaCenter(double[][] points, MissingDataMethod missingDataMethod, int fillingDeep)
        {
            int c = points.Length;
            if (c == 0)
                throw new BIGeometryCoordinatesCountMismatch(c, 1, null);
            //инициализируем центр
            double[] center = new double[points[0].Length];//центр множества            
            for (int i = 0; i < center.Length; i++)
            {
                center[i] = 0;                
            }            
            //заполняем пропуски
            double[] coordValues = new double[c];
            for (int j = 0; j < center.Length; j++)
            {                                
                for (int i = 0; i < c; i++)
                {
                    coordValues[i] = points[j][i];
                }
                FillGaps(ref coordValues, missingDataMethod, fillingDeep);
                for (int i = 0; i < c; i++)
                {
                    points[j][i] = coordValues[i];
                }
            }
            //считаем сумму по координатам
            for (int i = 0; i < c; i++)
            {                
                double[] coordinates = points[i];
                for (int j = 0; j < coordinates.Length; j++)
                {
                    if (j >= center.Length)
                        throw new BIGeometryCoordinatesCountMismatch(center.Length, coordinates.Length, null);
                    double coord = coordinates[j];
                    if (coord == Constants.NullDouble)
                    {
                        //пропуски все равно остались
                        throw new BIGeometryImpossibleCalculateCenterException(i, null);
                    }
                    center[j] += coord;
                }
            }
            //вычисляем центр
            for (int i = 0; i < center.Length; i++)
            {
                center[i] = center[i] / (double)c;
            }
            return center;
        }

        /// <summary>
        /// Возвращает прямоугольник, образованный двумя точками противоположных углов
        /// </summary>
        /// <param name="point1"></param>
        /// <param name="point2"></param>
        /// <returns></returns>
        public static Rectangle TwoPointsToRectangle(Point point1, Point point2)
        {
            Point firstPoint = point1.Y <= point2.Y ? point1 : point2;
            Point secondPoint = firstPoint.Equals(point1) ? point2 : point1;
            if (firstPoint.X > secondPoint.X)
            {
                //плохо; значит нам дали не те углы, что надо;
                //надо вычислять по оставшимся углам
                int xmem = firstPoint.X;
                firstPoint.X = secondPoint.X;
                secondPoint.X = xmem;
                return TwoPointsToRectangle(firstPoint, secondPoint);
            }
            secondPoint.Offset(GetNegativePoint(firstPoint));//заменили вторую точку на размер, чтоб не плодить переменных            
            return new Rectangle(firstPoint, new Size(secondPoint));
        }
    }
}
